为什么需要 SSL/TLS
当微服务越来越多时,服务之间的安全通信变得非常重要。虽然可以在 Nginx 等反向代理层配置证书实现 HTTPS,但在 gRPC 的服务间直接通信场景中,直接配置证书更加方便和安全。
gRPC 原生支持 TLS 加密通信,可以在传输层对数据进行加密,防止中间人攻击和数据泄露。
SSL 证书准备
1. 申请域名证书
使用 acme.sh(Let's Encrypt 的客户端)申请免费 SSL 证书:
# 安装 acme.sh(如果国外网站无法访问,可以使用国内镜像)
curl https://get.acme.sh | sh
# 申请证书(需要拥有域名并配置 DNS 解析)
acme.sh --issue -d your-domain.com --dns dns_cf
bash
2. 本地测试配置
在本地开发环境中,需要配置 hosts 文件将域名指向本地:
# /etc/hosts
127.0.0.1 grpc.your-domain.com
text
3. 证书文件说明
申请完成后会得到以下文件:
| 文件 | 说明 |
|---|---|
fullchain.cer | 完整证书链(包含服务器证书 + 中间证书) |
your-domain.com.key | 私钥文件 |
ca.cer | CA 证书(用于客户端验证) |
NestJS gRPC 配置 SSL
服务端配置
// main.ts
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { join } from 'path';
import * as fs from 'fs';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
url: 'grpc.your-domain.com:5000',
credentials: await loadSSLCredentials(),
},
},
);
await app.listen();
}
async function loadSSLCredentials() {
const { credentials } = await import('@grpc/grpc-js');
const rootCert = fs.readFileSync(join(__dirname, '../certs/ca.cer'));
const certChain = fs.readFileSync(join(__dirname, '../certs/fullchain.cer'));
const privateKey = fs.readFileSync(join(__dirname, '../certs/domain.key'));
return credentials.createSsl(rootCert, privateKey, certChain);
}
bootstrap();
typescript
客户端配置
// app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { join } from 'path';
import * as fs from 'fs';
@Module({
imports: [
ClientsModule.register([
{
name: 'HERO_PACKAGE',
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, '../shared/proto/hero.proto'),
url: 'grpc.your-domain.com:5000',
credentials: loadClientSSLCredentials(),
},
},
]),
],
})
export class AppModule {}
function loadClientSSLCredentials() {
const { credentials } = require('@grpc/grpc-js');
const rootCert = fs.readFileSync(join(__dirname, '../certs/ca.cer'));
return credentials.createSsl(rootCert);
}
typescript
使用 OpenSSL 调试证书
当证书配置出现问题时,可以使用 OpenSSL 命令来调试:
# 测试 SSL 连接
openssl s_client -connect grpc.your-domain.com:5000 -servername grpc.your-domain.com
# 查看证书信息
openssl x509 -in fullchain.cer -text -noout
# 验证私钥与证书是否匹配
openssl x509 -noout -modulus -in fullchain.cer | openssl md5
openssl rsa -noout -modulus -in domain.key | openssl md5
bash
常见证书问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
UNABLE_TO_VERIFY_LEAF_SIGNATURE | 缺少中间证书 | 使用 fullchain.cer 而非单独的服务器证书 |
CERT_HAS_EXPIRED | 证书过期 | 续期证书 |
ERR_TLS_CERT_ALTNAME_INVALID | 域名与证书不匹配 | 确保访问域名与证书域名一致 |
SELF_SIGNED_CERT_IN_CHAIN | 使用自签名证书 | 客户端需要信任该 CA |
↑